#include "cheetah/graphics/utils.h"

#include <GL/glew.h>
#include <GL/glut.h>
#include <stdio.h>
#include <map>

#include "cheetah/core/debug.h"
#include "cheetah/model/triangle_mesh.h"
#include "cheetah/graphics/texture_gl.h"
#include "cheetah/graphics/image.h"

//struct Edge {
//  GLuint pIndex[2]; //position indices
//
//  bool operator< (const Edge &rhs) const {
//    return ( pIndex[0] == rhs.pIndex[0]) ? ( pIndex[1] < rhs.pIndex[1]) : pIndex[0] < rhs.pIndex[0];
//  }
//
//  Edge( GLuint v0, GLuint v1) {
//    pIndex[0] = std::min( v0, v1);
//    pIndex[1] = std::max( v0, v1);
//  }
//
//private:
//  Edge() {} // disallow the default constructor
//};

namespace cheetah {
namespace graphics {
using namespace geom;
using namespace core;

//ModelGL::ModelGL() {
//  opts_ = 0;
//  glGenBuffers(1, &vertex_vbo_);
//  glGenBuffers(1, &normal_vbo_);  
//  glGenBuffers(1, &element_vbo_);
//}
//
//ModelGL::~ModelGL() {
//  glDeleteBuffers(1, &vertex_vbo_);
//  glDeleteBuffers(1, &normal_vbo_);
//  glDeleteBuffers(1, &element_vbo_);
//}
//
//void ModelGL::SetData(const TriangleMesh *mesh, const int opts) {
//  assert(NULL != mesh);
//  if (NULL == mesh) return;
//  mesh_ = mesh;
//  opts_ = opts;
//  bound_ = mesh_->Bound();
//  int vertex_size = mesh->n_vertices() * mesh->vertex_size;
//  int normal_size = mesh->n_vertices() * mesh->normal_size;
//
//  glBindBuffer(GL_ARRAY_BUFFER, vertex_vbo_);
//  glBufferData(GL_ARRAY_BUFFER, vertex_size , mesh->vertices(), GL_STATIC_DRAW);
//  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
//
//  glBindBuffer(GL_ARRAY_BUFFER, normal_vbo_);
//  glBufferData(GL_ARRAY_BUFFER, normal_size, mesh->normals(), GL_STATIC_DRAW);
//  glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, 0);
//
//  glBindBuffer(GL_ARRAY_BUFFER, 0);
//
//  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, element_vbo_);
//  glBufferData(GL_ELEMENT_ARRAY_BUFFER, mesh->n_triangles() * mesh->idx_size,
//               mesh->indices(), GL_STATIC_DRAW);
//  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
//  CheckErrorsGL("ModelGL::SetData");
//}
//
//bool ModelGL::Draw() {
//  if (!this->BeginDraw()) return false;
//  glDrawElements(GL_TRIANGLES, _3X(mesh_->n_triangles()), GL_UNSIGNED_INT,
//                 NULL);
//  CheckErrorsGL("ModelGL::Draw");
//  return this->EndDraw();
//}
//
//bool ModelGL::DrawInstanced(const int count) {
//  if (!this->BeginDraw()) return false;
//  glDrawElementsInstanced(GL_TRIANGLES, _3X(mesh_->n_triangles()),
//                          GL_UNSIGNED_INT, NULL, count);
//  return this->EndDraw();
//}
//
//inline bool ModelGL::BeginDraw() {
//  glBindBuffer(GL_ARRAY_BUFFER, vertex_vbo_);  
//  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
//  glBindBuffer(GL_ARRAY_BUFFER, normal_vbo_);  
//  glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 0, 0);
//
//  glEnableVertexAttribArray(0);
//  glEnableVertexAttribArray(2);
//  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, element_vbo_);
//  if (CheckErrorsGL("ModelGL::BeginDraw")) return false;
//  return true;
//}
//
//inline bool ModelGL::EndDraw() {
//  glDisableVertexAttribArray(0);
//  glDisableVertexAttribArray(2);
//  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
//
//  if (CheckErrorsGL("ModelGL::EndDraw")) return false;
//  return true;
//}
//
//bool ModelGL::DrawWidthAdjacency() {
//  /*glBindBuffer(GL_ARRAY_BUFFER, vert_array_id_);
//  glVertexPointer(3, GL_FLOAT, sizeof(Point), NULL);
//  glBindBuffer(GL_ARRAY_BUFFER, normal_array_id_);
//  glNormalPointer(GL_FLOAT, sizeof(Normal), NULL);
//  glBindBuffer(GL_ARRAY_BUFFER, 0);
//
//  glEnableClientState(GL_VERTEX_ARRAY);
//  glEnableClientState(GL_NORMAL_ARRAY);
//
//  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, element_array_id_);
//  glDrawElements(GL_TRIANGLES_ADJACENCY_EXT, _3X(mesh_->triangle_count()), 
//                 GL_UNSIGNED_INT, NULL);
//  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
//  glDisableClientState(GL_VERTEX_ARRAY);
//  glDisableClientState(GL_NORMAL_ARRAY);
//  if (CheckErrorsGL("ModelGL::Draw")) return false;*/
//  return true;
//}
//
//void ModelGL::DrawBound() {  
//  glBegin(GL_QUADS);
//  glVertex3f(bound_.minP.x, bound_.minP.y, bound_.minP.z);
//  glVertex3f(bound_.maxP.x, bound_.minP.y, bound_.minP.z);
//  glVertex3f(bound_.maxP.x, bound_.maxP.y, bound_.minP.z);
//  glVertex3f(bound_.minP.x, bound_.maxP.y, bound_.minP.z);
//
//  glVertex3f(bound_.minP.x, bound_.minP.y, bound_.maxP.z);
//  glVertex3f(bound_.maxP.x, bound_.minP.y, bound_.maxP.z);
//  glVertex3f(bound_.maxP.x, bound_.maxP.y, bound_.maxP.z);
//  glVertex3f(bound_.minP.x, bound_.maxP.y, bound_.maxP.z);
//
//  glVertex3f(bound_.minP.x, bound_.minP.y, bound_.minP.z);
//  glVertex3f(bound_.maxP.x, bound_.minP.y, bound_.minP.z);
//  glVertex3f(bound_.maxP.x, bound_.minP.y, bound_.maxP.z);
//  glVertex3f(bound_.minP.x, bound_.minP.y, bound_.maxP.z);
//
//  glVertex3f(bound_.minP.x, bound_.maxP.y, bound_.minP.z);
//  glVertex3f(bound_.maxP.x, bound_.maxP.y, bound_.minP.z);
//  glVertex3f(bound_.maxP.x, bound_.maxP.y, bound_.maxP.z);
//  glVertex3f(bound_.minP.x, bound_.maxP.y, bound_.maxP.z);
//  
//  glVertex3f(bound_.minP.x, bound_.minP.y, bound_.minP.z);
//  glVertex3f(bound_.minP.x, bound_.minP.y, bound_.maxP.z);
//  glVertex3f(bound_.minP.x, bound_.maxP.y, bound_.maxP.z);
//  glVertex3f(bound_.minP.x, bound_.maxP.y, bound_.minP.z);
//
//  glVertex3f(bound_.maxP.x, bound_.minP.y, bound_.minP.z);
//  glVertex3f(bound_.maxP.x, bound_.minP.y, bound_.maxP.z);
//  glVertex3f(bound_.maxP.x, bound_.maxP.y, bound_.maxP.z);
//  glVertex3f(bound_.maxP.x, bound_.maxP.y, bound_.minP.z);
//  glEnd();
//}
//
//void ModelGL::Initialize( const TriangleMesh *mesh, int opts /*= kTriangles*/ ) {
//  //typedef std::multimap<Edge, int> EdgeMap;
//  //bool need_edges = opts & kEdges;
//  //bool need_triangles_with_Adj = opts & kTrianglesWithAdjacency;
//  //if (need_edges || need_triangles_with_Adj) {
//  //  EdgeMap edges;
//  //  int size = static_cast<int>(mesh->vertex_count());
//  //  for (int i = 0; i < size; i += 3) { 
//  //    for (int j = 0; j < 3; ++j) {
//  //      Edge w(mesh->vertex_index(i+j), mesh->vertex_index(i+(j+1)%3));
//  //      EdgeMap::iterator it = edges.find(w);
//  //      if (need_edges && it == edges.end()) {
//  //        //add edge indices
//  //      }
//  //      edges.insert(w, i / 3);
//  //    }
//  //  }
//  //  if (need_triangles_with_Adj) {
//  //    for (int i = 0; i <size; i += 3) {
//  //      for (int j = 0; j < 3; ++j) {
//  //        Edge w(mesh->vertex_index(i+j), mesh->vertex_index(i+(j+1)%3));
//  //        EdgeMap::iterator it = edges.lower_bound(w);
//  //        EdgeMap::iterator limit = edges.upper_bound(w);
//  //        int adj_vertex = 0;
//  //        int triangle_idx = i / 3;
//  //        while (it != limit && it->second == triangle_idx) ++it;
//  //        if (it == limit || it->first.pIndex[0] != w.pIndex[0] || 
//  //            it->first.pIndex[1] != w.pIndex[i]) {
//  //          adj_vertex = mesh->vertex_index(i+j);
//  //          //++open_edges_;
//  //        } else {
//  //          int tri_offset = it->second * 3;
//  //          adj_vertex = mesh->vertex_index(tri_offset);
//  //          for (int k = 0; k < 3; ++k) {
//  //            if (m)
//  //          }
//  //        }
//  //      }
//  //    }
//  //  }
//  //}
//}

void DrawQuad(float x1, float y1, float x2, float y2) {
  glBegin(GL_QUADS);
  glTexCoord2f(x1, y1); glVertex2f(x1, y1);
  glTexCoord2f(x1, y2); glVertex2f(x1, y2);
  glTexCoord2f(x2, y2); glVertex2f(x2, y2);
  glTexCoord2f(x2, y1); glVertex2f(x2, y1);
  glEnd();
}

void DrawQuad(float x1, float y1, float z1,  float x2, float y2, float z2) {
  glBegin(GL_QUADS);
  glEnd();
}

void DrawQuad(float x1, float y1 ,float x2, float y2, float z) {
  glBegin(GL_QUADS);
  glTexCoord2f(0, 0); glVertex3f(x1, y1, z);
  glTexCoord2f(0, 1); glVertex3f(x1, y2, z);
  glTexCoord2f(1, 1); glVertex3f(x2, y2, z);
  glTexCoord2f(1, 0); glVertex3f(x2, y1, z);
  glEnd();
}

void DrawCube(const Point &p1, const Point &p2) {
  DrawCube(p1.x, p1.y, p1.z, p2.x, p2.y, p2.z);
}

void DrawCube(float size);

void DrawCube(float x1, float y1, float z1, float x2, float y2, float z2) {
  glBegin(GL_QUADS);

  glNormal3f(0, -1, 0); 
  glVertex3f(x1, y1, z1);
  glVertex3f(x2, y1, z1);
  glVertex3f(x2, y1, z2);
  glVertex3f(x1, y1, z2);

  glNormal3f(0, 1, 0); 
  glVertex3f(x1, y2, z1);
  glVertex3f(x1, y2, z2);
  glVertex3f(x2, y2, z2);
  glVertex3f(x2, y2, z1);

  glNormal3f(-1, 0, 0); 
  glVertex3f(x1, y1, z1);
  glVertex3f(x1, y1, z2);
  glVertex3f(x1, y2, z2);
  glVertex3f(x1, y2, z1);

  glNormal3f(1,0, 0); 
  glVertex3f(x2, y1, z1);
  glVertex3f(x2, y2, z1);
  glVertex3f(x2, y2, z2);
  glVertex3f(x2, y1, z2);

  glNormal3f(0, 0, -1); 
  glVertex3f(x1, y1, z1);
  glVertex3f(x1, y2, z1);
  glVertex3f(x2, y2, z1);
  glVertex3f(x2, y1, z1);

  glNormal3f(0, 0, 1);
  glVertex3f(x1, y1, z2);
  glVertex3f(x2, y1, z2);
  glVertex3f(x2, y2, z2);
  glVertex3f(x1, y2, z2);
  glEnd();
}

void DrawTextGL(int x, int y, const char *text) {
  int width = glutGet(GLUT_WINDOW_WIDTH);
  int height = glutGet(GLUT_WINDOW_HEIGHT);

  glPushAttrib(GL_DEPTH_BUFFER_BIT|GL_TRANSFORM_BIT);
  glDisable(GL_DEPTH_TEST);
  glMatrixMode(GL_PROJECTION);
  glPushMatrix();
  glLoadIdentity();
  gluOrtho2D(0, width, 0, height);
  glMatrixMode(GL_MODELVIEW);
  glPushMatrix();
  glLoadIdentity();

  glRasterPos2i(x, y);
  int len = strlen(text);
  for (int i = 0; i < len; ++i)
    glutBitmapCharacter(GLUT_BITMAP_9_BY_15, *(text +i));
  
  glMatrixMode(GL_PROJECTION);
  glPopMatrix();
  glMatrixMode(GL_MODELVIEW);
  glPopMatrix();
  glPopAttrib();
  glFlush();
}

GLuint MakeFullScreenQuad() {
  GLuint display_list = glGenLists(1);
  if (display_list) {
    glNewList(display_list, GL_COMPILE);
    
    // the vertex shader only need to pass the vertex through
    glMatrixMode(GL_PROJECTION);
    glPushMatrix();
    glLoadIdentity();
    gluOrtho2D(0, 1, 0, 1);
    glMatrixMode(GL_MODELVIEW);
    glPushMatrix();
    glLoadIdentity();
    //glRectf(-1, -1, 1, 1);
    DrawQuad(0, 0, 1, 1);
    glPopMatrix();
    glMatrixMode(GL_PROJECTION);
    glPopMatrix();
    glMatrixMode(GL_MODELVIEW);
    glEndList();
  }
  return display_list;
}

void DisplayStatistics(const char *name, float data, const char *unit, 
                       int x, int y) {
  char buff[256];
  sprintf(buff, "%s : %.3f %s", name, data, unit);
  glColor3f(0, 0, 0);
  DrawTextGL(x, y, buff);
  glColor3f(1, 1, 1);  
  DrawTextGL(x+1, y+1, buff);
}

void VisualizeTexture(const Texture2D &tex) {
  glPushAttrib(GL_ALL_ATTRIB_BITS);
  glMatrixMode(GL_PROJECTION);
  glPushMatrix();
  glLoadIdentity();
  gluOrtho2D(0, 1, 0, 1);
  glMatrixMode(GL_MODELVIEW);
  glPushMatrix();
  glLoadIdentity();
  glMatrixMode(GL_TEXTURE);
  glPushMatrix();
  glLoadIdentity();
  tex.Enable();
  tex.Bind();
  glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_NONE);
  // Show the shadowMap at its actual size relative to window

  // the following applys only to 2d texture, for rectangle texture
  // the tex quad should be corresponding to texture size
  DrawQuad(0.0, 0.0, 1.0, 1.0); 
  tex.Unbind();
  tex.Disable();
  glPopMatrix();
  glMatrixMode(GL_PROJECTION);
  glPopMatrix();
  glMatrixMode(GL_MODELVIEW);
  glPopMatrix();
  glPopAttrib();
}

bool CheckErrorsGL(const char* location) {	
  GLuint error_id;
  const char *error_str;
  bool error_occurred = false;
  while (error_id = glGetError()) {
    error_str = reinterpret_cast<const char*>(gluErrorString(error_id));
    Error("In \"%s\": error: %u, %s", location, error_id, error_str);
    error_occurred = true;
  }
  return error_occurred;
}

bool CheckGLInfo() {
  GLenum err = glewInit();
  if (GLEW_OK != err) {
    Error("glewInit", glewGetErrorString(err));
    return false;
  }
  
  debug::Log("VENDOR", glGetString(GL_VENDOR));
  debug::Log("RENDERER", glGetString(GL_RENDERER));
  debug::Log("VERSION", glGetString(GL_VERSION));

  GLint max_draw_buffers;
  glGetIntegerv(GL_MAX_DRAW_BUFFERS, &max_draw_buffers);
  debug::Log("max draw buffers", max_draw_buffers); 

  GLint max_texture_coords;
  glGetIntegerv(GL_MAX_TEXTURE_COORDS, &max_texture_coords);
  debug::Log("max textures coords", max_texture_coords);

  GLint max_textures;
  glGetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS , &max_textures);
  debug::Log("max textures", max_textures);
  
  if (!glewIsSupported("GL_EXT_gpu_shader4")) {
    debug::Log("GL_EXT_gpu_shader4 unsupported");
  }
  if (!glewIsSupported("GL_EXT_texture_integer")) {
    debug::Log("GL_EXT_texture_integer unsupported");
  }
  if (!glewIsSupported("GL_ARB_color_buffer_float")) {
    debug::Log("GL_EXT_texture_integer unsupported"); 
  }
  return true;
}

void SaveFrameBuffer(const char *filename) {
  const int w = glutGet(GLUT_WINDOW_WIDTH);
  const int h = glutGet(GLUT_WINDOW_HEIGHT);
  float *pixels = new float[w * h * 4];
  glReadPixels(0, 0, w, h, GL_RGBA, GL_FLOAT, pixels);
  SaveImage(filename, w, h, 1, 4, GL_RGBA, GL_FLOAT, pixels);
  delete[] pixels;
}

} // graphics
} // cheetah
